Update plugin ZTools 提供商 v1.0.1#309
Conversation
- feat: ZTools OCR + 翻译提供商插件(截图识别 / 代码翻译 / manage) - chore: 调整插件命名空间 - chore: 调整资源url - feat: 微信 OCR 原生模块支持 macOS(libwxocr.dylib) - feat: 截图识别结果改为独立窗口展示(左图右文 + 拖动缩放)
There was a problem hiding this comment.
Code Review
This pull request refactors the screen OCR feature to display results in a separate, borderless sub-window (screen-ocr-result.html) instead of the main plugin window. The main window now acts as an orchestrator that handles engine checking, capturing, and invoking the sub-window, while the new sub-window handles image rendering, zooming, panning, and text selection. The review feedback highlights several critical and medium-severity issues, including a redundant image-loading call that resets user zoom/pan on fallback data injection, potential crashes from un-checked display or destroyed window instances, a potential negative scale rendering bug in extreme viewport sizes, TypeScript type conflicts with setTimeout, and Vue 3 reactivity issues when using sparse arrays for tracking flash states.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if (typeof data.logo === 'string') logo.value = data.logo | ||
| if (data.image) nextTick(loadImage) | ||
| } |
There was a problem hiding this comment.
| const display = window.ztools.getPrimaryDisplay() | ||
| // workArea 同为 DIP,含任务栏扣除,比 size 更安全 | ||
| const workArea = display.workArea | ||
| const screenWDip = workArea.width | ||
| const screenHDip = workArea.height | ||
| // scaleFactor:如 1 / 1.25 / 1.5 / 1.9 / 2 | ||
| const scaleFactor = display.scaleFactor || 1 |
There was a problem hiding this comment.
在获取主显示器信息时,建议对 display 进行空值保护。在某些多显示器切换、无显示器或特定系统环境下,getPrimaryDisplay() 可能会返回 null 或 undefined,直接访问其属性会导致运行时崩溃。
const display = window.ztools.getPrimaryDisplay()
// workArea 同为 DIP,含任务栏扣除,比 size 更安全
const workArea = display?.workArea || { x: 0, y: 0, width: 1280, height: 800 }
const screenWDip = workArea.width
const screenHDip = workArea.height
// scaleFactor:如 1 / 1.25 / 1.5 / 1.9 / 2
const scaleFactor = display?.scaleFactor || 1
| /** 把数据注入子窗口:调用其 window.__loadScreenOcrResult。 */ | ||
| function injectData( | ||
| win: BrowserWindow.WindowInstance, | ||
| data: { image: string; lines: OcrLine[]; isDark: boolean; logo?: string } | ||
| ): void { | ||
| try { | ||
| // 序列化为安全的 JSON,避免引号/换行破坏 JS 字符串 | ||
| const payload = JSON.stringify(data) | ||
| const code = `window.__loadScreenOcrResult && window.__loadScreenOcrResult(${payload});` | ||
| win.webContents.executeJavaScript(code) | ||
| } catch (_) { | ||
| /* ignore:兜底注入会再试 */ | ||
| } | ||
| } |
There was a problem hiding this comment.
在向子窗口注入数据时,由于存在 800ms 的延迟兜底注入(setTimeout),如果用户在 800ms 内快速关闭了窗口,或者窗口创建失败返回了空值,直接调用 win.webContents 会抛出异常。建议在执行注入前,先检查 win 实例是否存在且未被销毁。
/** 把数据注入子窗口:调用其 window.__loadScreenOcrResult。 */
function injectData(
win: any,
data: { image: string; lines: OcrLine[]; isDark: boolean; logo?: string }
): void {
try {
if (!win || (typeof win.isDestroyed === 'function' && win.isDestroyed()) || !win.webContents) {
return
}
// 序列化为安全的 JSON,避免引号/换行破坏 JS 字符串
const payload = JSON.stringify(data)
const code = 'window.__loadScreenOcrResult && window.__loadScreenOcrResult(' + payload + ');'
win.webContents.executeJavaScript(code)
} catch (_) {
/* ignore:兜底注入会再试 */
}
}
| const copied = ref('') | ||
| let copiedTimer: number | null = null |
There was a problem hiding this comment.
| function computeFitScale(): number { | ||
| const vp = viewportRef.value | ||
| const vw = vp ? vp.clientWidth : window.innerWidth | ||
| const vh = vp ? vp.clientHeight : window.innerHeight | ||
| if (!naturalWidth.value || !naturalHeight.value || !vw || !vh) return 1 | ||
| // 留 16px 内边距 | ||
| const aw = vw - 16 | ||
| const ah = vh - 16 | ||
| return Math.min(aw / naturalWidth.value, ah / naturalHeight.value, 1) | ||
| } |
There was a problem hiding this comment.
在计算适应窗口的缩放比例时,如果视口宽度或高度小于 16px(例如在窗口最小化或极端缩放调整的瞬间),aw 或 ah 可能会计算为负数,从而导致 Math.min 返回负的缩放比例,使图片发生镜像翻转或渲染异常。建议使用 Math.max(1, ...) 确保可用宽高不为负数。
function computeFitScale(): number {
const vp = viewportRef.value
const vw = vp ? vp.clientWidth : window.innerWidth
const vh = vp ? vp.clientHeight : window.innerHeight
if (!naturalWidth.value || !naturalHeight.value || !vw || !vh) return 1
// 留 16px 内边距,确保不为负数
const aw = Math.max(1, vw - 16)
const ah = Math.max(1, vh - 16)
return Math.min(aw / naturalWidth.value, ah / naturalHeight.value, 1)
}
| // 闪烁标记:点击图上文字复制后,用 id 标记该行闪烁高亮(递增避免重复定时器冲突)。 | ||
| const flashedIds = ref<number[]>([]) |
| /** 触发第 i 行的闪烁高亮(图上 + 列表),1.5s 后移除。 */ | ||
| function flashLine(i: number): void { | ||
| activeIndex.value = i | ||
| const id = (flashedIds.value[i] ?? 0) + 1 | ||
| flashedIds.value[i] = id | ||
| window.setTimeout(() => { | ||
| if (flashedIds.value[i] === id) flashedIds.value[i] = 0 | ||
| }, 1500) | ||
| } |
There was a problem hiding this comment.
配合将 flashedIds 重构为 Record<number, number>,更新 flashLine 函数。在定时器结束时,可以直接删除对应的键,保持响应式对象的干净整洁。
/** 触发第 i 行的闪烁高亮(图上 + 列表),1.5s 后移除。 */
function flashLine(i: number): void {
activeIndex.value = i
const id = (flashedIds.value[i] ?? 0) + 1
flashedIds.value[i] = id
window.setTimeout(() => {
if (flashedIds.value[i] === id) {
delete flashedIds.value[i]
}
}, 1500)
}
插件信息
本次变更
截图 / 演示
自检清单
plugins/f-provider/目录此 PR 由 ztools-plugin-cli 自动管理:每次
ztools publish在分支上追加一个 commit,PR 链接保持不变。